[Python] 実例を見て分かるSet型の使い方
こんにちは。サービス開発室の武田です。
前回、集合型便利だよ!というお話をしました。
「便利そうなのはわかったけど、実際どう使うの?」という声が聞こえてきそうですね。そんなわけで、実例を交え紹介します。
Set型を利用した実例
boto3を使用した例に偏っているのは最近書いているコードがこういうものだからです。ご承知おきください。
指定のサービスが使用できるリージョンを求める
boto3にはget_available_regions()
という、指定したサービスがサポートされているリージョンの一覧を取得するメソッドがあります。たとえばdetective
であれば次のような結果が得られます。
>>> import boto3 >>> session = boto3.session.Session() >>> session.get_available_regions("detective") ['af-south-1', 'ap-east-1', 'ap-northeast-1', 'ap-northeast-2', 'ap-south-1', 'ap-southeast-1', 'ap-southeast-2', 'ca-central-1', 'eu-central-1', 'eu-north-1', 'eu-south-1', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'il-central-1', 'me-south-1', 'sa-east-1', 'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2']
またdescribe_regions()
という、アカウントで利用できるリージョンの一覧を取得するメソッドもあります。
>>> ec2 = boto3.client("ec2") >>> ec2.describe_regions()["Regions"] [{'Endpoint': 'ec2.ap-south-1.amazonaws.com', 'RegionName': 'ap-south-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.eu-north-1.amazonaws.com', 'RegionName': 'eu-north-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.eu-west-3.amazonaws.com', 'RegionName': 'eu-west-3', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.eu-west-2.amazonaws.com', 'RegionName': 'eu-west-2', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.eu-west-1.amazonaws.com', 'RegionName': 'eu-west-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.ap-northeast-3.amazonaws.com', 'RegionName': 'ap-northeast-3', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.ap-northeast-2.amazonaws.com', 'RegionName': 'ap-northeast-2', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.ap-northeast-1.amazonaws.com', 'RegionName': 'ap-northeast-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.ca-central-1.amazonaws.com', 'RegionName': 'ca-central-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.sa-east-1.amazonaws.com', 'RegionName': 'sa-east-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.ap-southeast-1.amazonaws.com', 'RegionName': 'ap-southeast-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.ap-southeast-2.amazonaws.com', 'RegionName': 'ap-southeast-2', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.eu-central-1.amazonaws.com', 'RegionName': 'eu-central-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.us-east-1.amazonaws.com', 'RegionName': 'us-east-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.us-east-2.amazonaws.com', 'RegionName': 'us-east-2', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.us-west-1.amazonaws.com', 'RegionName': 'us-west-1', 'OptInStatus': 'opt-in-not-required'}, {'Endpoint': 'ec2.us-west-2.amazonaws.com', 'RegionName': 'us-west-2', 'OptInStatus': 'opt-in-not-required'}]
これらを利用すれば「指定のアカウントで指定のサービス(ここではDetective)がサポートされているリージョン」を求められます。
>>> {r["RegionName"] for r in ec2.describe_regions()["Regions"]} & set(session.get_available_regions("detective")) {'eu-west-3', 'sa-east-1', 'us-east-1', 'ap-southeast-2', 'ap-southeast-1', 'eu-west-2', 'us-west-2', 'eu-north-1', 'us-east-2', 'us-west-1', 'ap-northeast-1', 'ap-northeast-2', 'ap-south-1', 'eu-west-1', 'eu-central-1', 'ca-central-1'}
反対に、「有効なリージョンのうち、Detectiveがサポートされていないリージョン」も求められます。
>>> {r["RegionName"] for r in ec2.describe_regions()["Regions"]} - set(session.get_available_regions("detective")) {'ap-northeast-3'}
ちなみに{n for n in ns}
という表記は集合内包表記というもので、コレクションから新しい集合を生成できます。リスト内包表記の集合版です。
うーん、便利!
重複のない一覧を作成する
そもそもの話として、他のコレクションにないSet型の特徴は重複のないコレクションということです。たとえばS3バケットに次のようなデータが保存されているとします。JSON Linesという形式です。
{"id":"aaa","email":"takeda1@example.com"} {"id":"bbb","email":"takeda2@example.com"} {"id":"ccc","email":"takeda3@example.com"} {"id":"ddd","email":"takeda1@example.com"}
このファイルをもとに、重複のないメールアドレスの一覧は次のように作成できます。
res = boto3.client("s3").select_object_content( Bucket="xxx", Key="yyy", InputSerialization={ "JSON": { "Type": "LINES", } }, OutputSerialization={"CSV": {"RecordDelimiter": "\n", "FieldDelimiter": ","}}, ExpressionType="SQL", Expression="SELECT s.email FROM s3object s", ) content = b"".join( event["Records"]["Payload"] for event in res["Payload"] if "Records" in event ).decode("UTF-8") email_set = {c for c in content.strip().split("\n")}
SQLに詳しい方はDISTINCT
使えばいいじゃん、と考えるでしょうか。しかし、残念ながらS3 SelectではDISTINCT
は使えません。そんなわけで、取得側で工夫する必要があります。
うーん、便利!
ロールに所定のポリシーがアタッチされているかの確認
利用しているロールにアタッチされているべきポリシーがあるとします。Set型を使えば簡単にチェックできます。
>>> regulation_policyes = {"AAA", "BBB", "CCC"} >>> iam = boto3.client("iam") >>> set(iam.list_role_policies(RoleName="test-role")["PolicyNames"]) >= regulation_policyes
アタッチされていないポリシーも簡単に出せます。
>>> regulation_policyes - set(iam.list_role_policies(RoleName="test-role")["PolicyNames"])
うーん、便利!
権限の強いポリシーがアタッチされているかの確認
先ほどの例と似ていますが、IAMユーザーにアタッチされているポリシーのうち、権限の高いポリシーがあるかのチェックも可能です。ユーザーに直接アタッチされているポリシーだけでなく、属しているグループの権限もチェックが必要です。簡略化のためlist_xxxx()
メソッドはそのまま書いています。実際には一度にすべての結果が返ってこない可能性もあるため、プロダクトコードではIsTruncated
フラグのチェックを必ずしてください。
>>> strong_authority = {"AdministratorAccess", "PowerUserAccess", "IAMFullAccess"} >>> iam = boto3.client("iam") >>> group_attached_policies = {p["PolicyName"] for g in iam.list_groups_for_user(UserName="test-user")["Groups"] for p in iam.list_attached_group_policies(GroupName=g["GroupName"])["AttachedPolicies"]} >>> user_attached_policies = {p["PolicyName"] for p in iam.list_attached_user_policies(UserName="test-user")["AttachedPolicies"]} >>> (user_attached_policies | group_attached_policies).isdisjoint(strong_authority)
うーん、便利!
まとめ
Set型による操作のイメージがちょっとずつでもついたでしょうか。複数の値どうしで何かしらの操作をする際には、集合でうまいことできないかな?と考えてみてください。